Table of Contents
This tutorial is about how to move objects around. It will introduce new shader techniques.
The simplest way one might think to move a triangle or other object around is to
simply modify the vertex position data directly. From previous tutorials, we learned
that the vertex data is stored in a buffer object. So the task is to modify the vertex
data in the buffer object. This is what cpuPositionOffset.cpp
does.
The modifications are done in two steps. The first step is to generate the X, Y offset
that will be applied to each position. The second is to apply that offset to each vertex
position. The generation of the offset is done with the
ComputePositionOffset
function:
Example 3.1. Computation of Position Offsets
void ComputePositionOffsets(float &fXOffset, float &fYOffset) { const float fLoopDuration = 5.0f; const float fScale = 3.14159f * 2.0f / fLoopDuration; float fElapsedTime = glutGet(GLUT_ELAPSED_TIME) / 1000.0f; float fCurrTimeThroughLoop = fmodf(fElapsedTime, fLoopDuration); fXOffset = cosf(fCurrTimeThroughLoop * fScale) * 0.5f; fYOffset = sinf(fCurrTimeThroughLoop * fScale) * 0.5f; }
This function computes offsets in a loop. The offsets produce circular motion, and the
offsets will reach the beginning of the circle every 5 seconds (controlled by
fLoopDuration
). The function
glutGet(GLUT_ELAPSED_TIME)
retrieves the integer time in
milliseconds since the application started. The fmodf
function
computes the floating-point modulus of the time. In lay terms, it takes the first
parameter and returns the remainder of the division between that and the second
parameter. Thus, it returns a value on the range [0, fLoopDuration
),
which is what we need to create a periodically repeating pattern.
The cosf
and sinf
functions compute the
cosine and sine respectively. It is not important to know exactly how these functions
work, but they effectively compute a circle of diameter 2. By multiplying by 0.5f, it
shrinks the circle down to a circle with a diameter of 1.
Once the offsets are computed, the offsets have to be added to the vertex data. This
is done with the AdjustVertexData
function:
Example 3.2. Adjusting the Vertex Data
void AdjustVertexData(float fXOffset, float fYOffset) { std::vector<float> fNewData(ARRAY_COUNT(vertexPositions)); memcpy(&fNewData[0], vertexPositions, sizeof(vertexPositions)); for(int iVertex = 0; iVertex < ARRAY_COUNT(vertexPositions); iVertex += 4) { fNewData[iVertex] += fXOffset; fNewData[iVertex + 1] += fYOffset; } glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertexPositions), &fNewData[0]); glBindBuffer(GL_ARRAY_BUFFER, 0); }
This function works by copying the vertex data into a std::vector, then applying the offset to the X and Y coordinates of each vertex. The last three lines are the OpenGL-relevant parts.
First, the buffer objects containing the positions is bound to the context. Then the
new function glBufferSubData
is called to transfer this data to the
buffer object.
The difference between glBufferData
and
glBufferSubData
is that the SubData function does not
allocate memory. glBufferData
specifically
allocates memory of a certain size; glBufferSubData
only transfers
data to the already existing memory. Calling glBufferData
on a
buffer object that has already been allocated tells OpenGL to
reallocate this memory, throwing away the previous data and
allocating a fresh block of memory. Whereas calling glBufferSubData
on a buffer object that has not yet had memory allocated by
glBufferData
is an error.
Think of glBufferData
as a combination of
malloc
and memcpy
, while glBufferSubData
is just memcpy
.
The glBufferSubData
function can update only a portion of the
buffer object's memory. The second parameter to the function is the byte offset into the
buffer object to begin copying to, and the third parameter is the number of bytes to
copy. The fourth parameter is our array of bytes to be copied into that location of the
buffer object.
The last line of the function is simply unbinding the buffer object. It is not strictly necessary, but it is good form to clean up binds after making them.
Buffer Object Usage Hints. Every time we draw something, we are changing the buffer object's data. OpenGL has
a way to tell it that you will be doing something like this, and it is the purpose
of the last parameter of glBufferData
. This tutorial changed
the allocation of the buffer object slightly, replacing:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
with this:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STREAM_DRAW);
GL_STATIC_DRAW
tells OpenGL that you intend to only set the data in
this buffer object once. GL_STREAM_DRAW
tells OpenGL that you intend
to set this data constantly, generally once per frame. These parameters do not mean
anything with regard to the API; they are simply hints to the
OpenGL implementation. Proper use of these hints can be crucial for getting good buffer
object performance when making frequent changes. We will see more of these hints
later.
The rendering function now has become this:
Example 3.3. Updating and Drawing the Vertex Data
void display() { float fXOffset = 0.0f, fYOffset = 0.0f; ComputePositionOffsets(fXOffset, fYOffset); AdjustVertexData(fXOffset, fYOffset); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(theProgram); glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0); glDrawArrays(GL_TRIANGLES, 0, 3); glDisableVertexAttribArray(0); glUseProgram(0); glutSwapBuffers(); glutPostRedisplay(); }
The first three lines get the offset and set the vertex data. Everything but the last
line is unchanged from the first tutorial. The last line of the function is there to
tell FreeGLUT to constantly call display
. Ordinarily,
display
would only be called when the window's size changes or
when the window is uncovered. glutPostRedisplay
causes FreeGLUT to
call display
again. Not immediately, but reasonably fast.
If you run the tutorial, you will see a smaller triangle (the size was reduced in this tutorial) that slides around in a circle.